package MelbourneMap;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.swing.JLabel;
import javax.swing.JPanel;

import geotransform.coords.*;
import geotransform.ellipsoids.*;
import geotransform.transforms.*;


/** 
 * DrawManager is the class that is used to manage what is
 * to be drawn where and determines the transformations that
 * are to take place. The DrawManager tells the {@link DrawPanel} 
 * object when to draw and what should be drawn with what 
 * transformations.It holds a list of {@link Region} objects
 * that need to be drawn and an {@link AffineTransform} object
 * that is used to apply transformations to anything that needs
 * drawing so that it is correctly displayed.
 * 
 * @author David Zappia, 7183216
 * @author Heong Jin Yu, 7019777
 *
 */
public class DrawManager implements ThreadCompleteListener
{	
	private MelbourneMap mm;
	private Graph roadGraph;
	private Graph railGraph;
	private DrawPanel panel;
	private DrawPanel viewPanel;
	private Map<String, Region>[] regionList;
	private AffineTransform world2pixel;
	private AffineTransform viewAT;
	
	private MouseHandler MHandler;
	
	private double scaleX;
	private double scaleY;
	private double zoom;
	private double panX;
	private double panY;
	
	//private ArrayList<Vehicle> vehicles = new ArrayList<Vehicle>();
	private Node pathNode1 = null;
	private Node pathNode2 = null;
	
	private Thread pmThread = null;
	
	/**
	 * Class constructor
	 */
	public DrawManager(MelbourneMap pMM, Graph roadGraph, Graph railGraph)
	{
		mm = pMM;
		this.roadGraph = roadGraph;
		this.railGraph = railGraph;
		regionList = new HashMap[5];
		zoom = 1;
		panel = new DrawPanel();
		viewPanel = new DrawPanel();
		
		world2pixel = new AffineTransform();
		viewAT = new AffineTransform();
		
		MHandler = new MouseHandler();
		
		panel.addMouseListener(MHandler);
		panel.addMouseMotionListener(MHandler);
		panel.addMouseWheelListener(MHandler);
		
		pmThread = new Thread(new PathManager());
		
	}
	
	/**
	 * Updates the {@link AffineTransform} object that is
	 * applied during the {@link DrawPanel} object paint
	 * process. The values are adjusted according to zoom
	 * and pan values as adjusted by the user. A complete
	 * set of translation and scale matrixes are combined
	 * to create the desired outcome.
	 */ 
	private void updateAT()
	{
		Dimension d = panel.getSize();
		double panelW = d.width;
        double panelH = d.height;
        
		Rectangle2D r = regionList[0].get("Victoria").getShape().getBounds2D();
		
		scaleX = (panelW/r.getWidth()) * zoom;
		scaleY = (panelH/r.getHeight()) * zoom;
		
		AffineTransform goToOrigin = AffineTransform.getTranslateInstance(-r.getMinX(), -r.getMinY());
		
		AffineTransform pan = AffineTransform.getTranslateInstance(panX * zoom, panY * zoom);

		AffineTransform scaleAndFlip = AffineTransform.getScaleInstance(scaleX, -scaleX);

		AffineTransform centre = AffineTransform.getTranslateInstance(panelW/2, panelH/2);
		centre.translate(-((r.getWidth()*scaleX)/2), ((r.getHeight()*scaleY)/2));
		
		world2pixel.setToIdentity();
		world2pixel.concatenate(pan);
		world2pixel.concatenate(centre);
		world2pixel.concatenate(scaleAndFlip);
		world2pixel.concatenate(goToOrigin);
	}
	
	private void viewAT()
	{
		Dimension d = viewPanel.getSize();
		int panelW = d.width;
        int panelH = d.height;
        
        Dimension d2 = panel.getSize();
        double rateX = (double)panelW / (double)d2.width;
        double rateY = (double)panelH / (double)d2.height;
        
		Rectangle2D r = regionList[0].get("Victoria").getShape().getBounds2D();
		
		scaleX = (panelW/r.getWidth()) * 15;
		scaleY = (panelH/r.getHeight()) * 15;
		
		AffineTransform goToOrigin = AffineTransform.getTranslateInstance(-r.getMinX(), -r.getMinY());
		
		AffineTransform pan = AffineTransform.getTranslateInstance(panX * rateX * 15, panY * rateY * 15);

		AffineTransform scaleAndFlip = AffineTransform.getScaleInstance(scaleX, -scaleX);
		
		AffineTransform centre = AffineTransform.getTranslateInstance(panelW/2, panelH/2 + 300);
		centre.translate(-((r.getWidth()*scaleX)/2), ((r.getHeight()*scaleY)/2));
		
		viewAT.setToIdentity();
		viewAT.concatenate(pan);
		viewAT.concatenate(centre);
		viewAT.concatenate(scaleAndFlip);
		viewAT.concatenate(goToOrigin);
	}
	
	/**
	 * Updates the entire drawing process from updating
	 * the transformation to setting the {@link Region}
	 * objects to be drawn and causing the {@link DrawPanel}
	 * object to repaint. To be called whenever an
	 * adjustment is made to zoom, pan or anything else
	 * that will affect the drawing.
	 */
	public void updateDraw()
	{
		updateAT();
		viewAT();
		
		panel.setAffine(world2pixel);
		panel.setShapes(regionList);
		panel.repaint();
		
		viewPanel.setAffine(viewAT);
		viewPanel.setShapes(regionList);
		viewPanel.repaint();
		
	}
	
	/**
	 * Adds a {@link Region} object to the current list of regions
	 * to be drawn at the specified index.
	 * 
	 * @param index		the index at which the region is added
	 * @param region	the Region object to add
	 * @see 			Region
	 */
	public void addRegion(int index, Map<String, Region> region)
	{
		regionList[index] = region;
		updateDraw();
	}
	
	/**
	 * Removes the {@link Region} object at the specified index
	 * from the list of regions to be drawn by setting the index
	 * to null.
	 * 
	 * @param index		the index of the region to be removed
	 */
	public void removeRegion(int index)
	{
		regionList[index] = null;
		updateDraw();
	}
	
	/**
	 * Returns the current {@link AffineTransform} object
	 * being used.
	 * 
	 * @return		the AffineTransform object being used
	 */
	public AffineTransform getAT()
	{
		return world2pixel;
	}
	
	/**
	 * Public call used by other objects to force the
	 * renewal of the {@link AffineTransform} object
	 */
	public void renewAT()
	{
		updateAT();
	}
	
	/**
	 * Increments the zoom level applied in the
	 * {@link AffineTransform} object by a preset
	 * amount.
	 */
	public void zoomIn()
	{
		if (zoom < 200)
		{
			zoom += 2;
			pan(0,0);
			updateDraw();
		}
	}
	
	/**
	 * Decrements the zoom level applied in the
	 * {@link AffineTransform} object by a preset
	 * amount.
	 */
	public void zoomOut()
	{
		if (zoom > 2)
		{
			zoom -= 2;
			pan(0,0);
			updateDraw();
		}
		
	}
	
	/**
	 * Adjusts the zoom level applied in the
	 * {@link AffineTransform} object by a passed
	 * parameter value.
	 * @param zoomVal zoom level
	 */
	public void zoom(int zoomVal)
	{
		if (zoomVal > 0 && zoomVal <= 200)
		{
			zoom = zoomVal;
			pan(0,0);
			updateDraw();
		}
		else if(zoomVal == 0)
		{
			zoom = 1;
			pan(0,0);
			updateDraw();
		}
		
	}
	
	/**
	 * Adjusts the x and y values applied in the 
	 * {@link AffineTransform} object. Both negative
	 * and positive values can be  used to adjust the
	 * transform appropriately.
	 * 
	 * @param dx		the amount to move in the x axis
	 * @param dy		the amount to move in the y axis
	 */
	public void pan(double dx, double dy)
	{
		panX += dx/zoom;
		panY += dy/zoom;
		updateDraw();
	}
	
	/**
	 * Resets the zoom and pan values to the original
	 * settings causing the entire drawing to fit
	 * centred in the window.
	 */
	public void reset()
	{
		panX = 0;
		panY = 0;
		zoom = 1;
		updateDraw();
	}
	
	/**
	 * Gets the inverse of the current {@link AffineTransform} object
	 * so that conversions can be made from the pixel point back to
	 * true world coordinates.
	 * 
	 * @return		the inverse of the current AffineTransform object
	 * @see			AffineTransform
	 */
	public AffineTransform getInverseAT()
	{
		try 
		{
			return world2pixel.createInverse();
		}
		catch (NoninvertibleTransformException e) 
		{
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Gets the current {@link DrawPanel} object that is
	 * bing used to draw.
	 * 
	 * @return		the DrawPanel object being used to draw
	 * @see			DrawPanel
	 */
	public DrawPanel getDrawPanel()
	{
		return panel;
	}
	
	public DrawPanel getViewPanel()
	{
		return viewPanel;
	}
	
	public void makePath()
	{
		Vehicle newVehicle = new Vehicle(roadGraph, pathNode1, pathNode2, 0, true);
		newVehicle.addListener(this);
		Thread aT = new Thread(newVehicle);
		//vehicles.add(newVehicle);
		panel.addVehicle(newVehicle);
		
		aT.start();
		
		if(panel.getVehicle().size() == 1)
			pmThread.start();
	}
	
	
	private class MouseHandler implements MouseMotionListener, MouseListener, MouseWheelListener
	{	
  		double xx = 0;
  		double yy = 0;
  		double dx = 0;
  		double dy = 0;
  		
  		Point mUtm;
  		
  		double radius = 10;
  		
  		Node selectedNode = null;
  		Node addNode = null;
  		
  		boolean adding;
  		
  		public MouseHandler()
  		{
  			mUtm = new Point();
  			adding = false;
  		}
  		
		public void mouseClicked(MouseEvent e) 
		{
			//Mouse right click, invoke dialog
			if (e.getButton() == MouseEvent.BUTTON3 && !mm.editCheck.isSelected())
				mm.doDialog(mUtm.x, mUtm.y, "Add point at");
			//if (mm.pathCheck.isSelected())
				
		}

		public void mouseEntered(MouseEvent e) {}

		public void mouseExited(MouseEvent e) {}

		public void mousePressed(MouseEvent e) 
		{
  			xx = e.getX();
  			yy = e.getY();
  			
  			//Search Road Nodes
  			if (e.getButton() == MouseEvent.BUTTON1 && (mm.editCheck.isSelected() && mm.roadJR.isSelected()) || (mm.pathCheck.isSelected()))
			{
				ArrayList<Node> roadNodes = roadGraph.getNodeList();
				
				Point oriNode = new Point();
				Point nodePoint = new Point();
				for(Node n : roadNodes)
				{
					oriNode.setLocation(n.getX(), n.getY());
					getAT().transform(oriNode, nodePoint);
					if ( Math.pow(nodePoint.getX() - e.getX(),  2) + Math.pow(nodePoint.getY() - e.getY(), 2) < Math.pow(radius*(zoom/70), 2) && n.editable)
					{
						selectedNode = n;
						if(mm.addCheck.isSelected())
						{
							addNode = n;
							break;
						}
						
						
					}
					else if ( Math.pow(nodePoint.getX() - e.getX(),  2) + Math.pow(nodePoint.getY() - e.getY(), 2) < Math.pow(radius*(zoom/70), 2) && mm.pathCheck.isSelected() )
					{
						if(pathNode1 != null && pathNode2 != null)
							break;
						else if(pathNode1==null)
						{
							pathNode1 = n;
							System.out.println("path1 Checked");
							break;
						}
						else if(pathNode2==null)
						{
							pathNode2 = n;
							System.out.println("path2 Checked");
							mm.goButton.setEnabled(true);
							break;
						}
					}
					
				}
				
				
				if(addNode != null)
				{
					if(adding)
					{
						Point newPoint = new Point();
					
						getInverseAT().transform(new Point(e.getX(), e.getY()), newPoint);
						
						Node n1 = roadGraph.getNode(addNode.getID()); 
	        			Node n2 = roadGraph.addNode(new Node(-1, newPoint.getX(), newPoint.getY()));
	        			
	        			n1.editable = true;
	        			n2.editable = true;
	        			roadGraph.addEdge(new Edge(n1, n2));
	        			mm.selectedReg.addNode(n1, n2);
	        			addNode = null;
	        			adding = false;
	        			updateDraw();
					}
					else
						adding = true;
				}
			}
  			
  			//Search Rail Nodes
  			else if(e.getButton() == MouseEvent.BUTTON1 && mm.editCheck.isSelected() && mm.railJR.isSelected())
  			{
  				ArrayList<Node> railNodes = railGraph.getNodeList();
  				
  				Point oriNode = new Point();
				Point nodePoint = new Point();
				for(Node n : railNodes)
				{
					oriNode.setLocation(n.getX(), n.getY());
					getAT().transform(oriNode, nodePoint);
					if ( Math.pow(nodePoint.getX() - e.getX(),  2) + Math.pow(nodePoint.getY() - e.getY(), 2) < Math.pow(radius*(zoom/70), 2) && n.editable)
					{
						selectedNode = n;
						break;
					}
				}
  			}
		}

		public void mouseReleased(MouseEvent e) 
		{
			selectedNode = null;
		}

		public void mouseDragged(MouseEvent e) 
		{

			if(!mm.editCheck.isSelected())
			{
				dx = e.getX()-xx;
				dy = e.getY()-yy;
				pan(dx, dy);
  				xx += dx;
  				yy += dy;
			}
			else if(selectedNode != null && mm.editCheck.isSelected())
			{
				Point mPoint = new Point(e.getX(), e.getY());
				Point nodePoint = new Point();
				getInverseAT().transform(mPoint, nodePoint);
				selectedNode.setLocation(nodePoint);
				selectedNode.update();
			}
			adding = false;
			addNode = null;
  			updateDraw();
  			mm.renewPoint();
		}

		public void mouseMoved(MouseEvent e) 
		{
			Point mouse = new Point(e.getX(), e.getY());

			getInverseAT().transform(new Point(e.getX(), e.getY()), mUtm);
			
			//Lon & Lat
			Gdc_Coord_3d gdc = new Gdc_Coord_3d();
			Utm_Coord_3d utm = new Utm_Coord_3d((double)mUtm.x, (double)mUtm.y,0, (byte)55, false);
			
			Utm_To_Gdc_Converter.Init(new IN_Ellipsoid());
			Utm_To_Gdc_Converter.Convert(utm, gdc);
			
			double latitude = Double.parseDouble(String.format("%.3f", gdc.latitude));
			double longitude = Double.parseDouble(String.format("%.3f", gdc.longitude));
			
			mm.setTitle("Melbourne Map || Pixel x. " + mouse.x + " y." + mouse.y + " || UTM x." + mUtm.x + " y." + mUtm.y + " || Latitude. " + latitude + " Longitude. " + longitude);
		}

		public void mouseWheelMoved(MouseWheelEvent e) 
		{
			int notches = e.getWheelRotation();
			if (notches < 0)
			{
				zoomIn();
				mm.slider.setValue(mm.slider.getValue()+2);
			}
			else
			{
				zoomOut();
				mm.slider.setValue(mm.slider.getValue()-2);
			}
			mm.renewPoint();
			
		}
		
	}
	
	@Override
	public void notifyOfThreadComplete(NotifyingThread notifyingThread) 
	{
		panel.getVehicle().remove(notifyingThread);
	}
	
	class PathManager implements Runnable
	{
		private boolean running;
		
		PathManager()
		{
			running = true;
		}

		public void run() {
			while (panel.getVehicle().size() > 0)
			{
				updateDraw();
				
				try {
					Thread.sleep(300);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			updateDraw();
			
		}
		
		
	}
	
	public void resetPath()
	{
		pathNode1 = null;
		pathNode2 = null;
	}
}
